//go:build enterprise || pro

package xorm

import (
	"database/sql"
	"fmt"
	"strconv"
	"strings"

	spannerclient "cloud.google.com/go/spanner"
	_ "github.com/googleapis/go-sql-spanner"
	spannerdriver "github.com/googleapis/go-sql-spanner"
	"google.golang.org/grpc/codes"
	"xorm.io/core"
)

func init() {
	core.RegisterDriver("spanner", &spannerDriver{})
	core.RegisterDialect("spanner", func() core.Dialect { return &spanner{} })
}

// https://cloud.google.com/spanner/docs/reference/standard-sql/lexical#reserved_keywords
var spannerReservedKeywords = map[string]struct{}{
	"ALL":                  {},
	"AND":                  {},
	"ANY":                  {},
	"ARRAY":                {},
	"AS":                   {},
	"ASC":                  {},
	"ASSERT_ROWS_MODIFIED": {},
	"AT":                   {},
	"BETWEEN":              {},
	"BY":                   {},
	"CASE":                 {},
	"CAST":                 {},
	"COLLATE":              {},
	"CONTAINS":             {},
	"CREATE":               {},
	"CROSS":                {},
	"CUBE":                 {},
	"CURRENT":              {},
	"DEFAULT":              {},
	"DEFINE":               {},
	"DESC":                 {},
	"DISTINCT":             {},
	"ELSE":                 {},
	"END":                  {},
	"ENUM":                 {},
	"ESCAPE":               {},
	"EXCEPT":               {},
	"EXCLUDE":              {},
	"EXISTS":               {},
	"EXTRACT":              {},
	"FALSE":                {},
	"FETCH":                {},
	"FOLLOWING":            {},
	"FOR":                  {},
	"FROM":                 {},
	"FULL":                 {},
	"GROUP":                {},
	"GROUPING":             {},
	"GROUPS":               {},
	"HASH":                 {},
	"HAVING":               {},
	"IF":                   {},
	"IGNORE":               {},
	"IN":                   {},
	"INNER":                {},
	"INTERSECT":            {},
	"INTERVAL":             {},
	"INTO":                 {},
	"IS":                   {},
	"JOIN":                 {},
	"LATERAL":              {},
	"LEFT":                 {},
	"LIKE":                 {},
	"LIMIT":                {},
	"LOOKUP":               {},
	"MERGE":                {},
	"NATURAL":              {},
	"NEW":                  {},
	"NO":                   {},
	"NOT":                  {},
	"NULL":                 {},
	"NULLS":                {},
	"OF":                   {},
	"ON":                   {},
	"OR":                   {},
	"ORDER":                {},
	"OUTER":                {},
	"OVER":                 {},
	"PARTITION":            {},
	"PRECEDING":            {},
	"PROTO":                {},
	"RANGE":                {},
	"RECURSIVE":            {},
	"RESPECT":              {},
	"RIGHT":                {},
	"ROLLUP":               {},
	"ROWS":                 {},
	"SELECT":               {},
	"SET":                  {},
	"SOME":                 {},
	"STRUCT":               {},
	"TABLESAMPLE":          {},
	"THEN":                 {},
	"TO":                   {},
	"TREAT":                {},
	"TRUE":                 {},
	"UNBOUNDED":            {},
	"UNION":                {},
	"UNNEST":               {},
	"USING":                {},
	"WHEN":                 {},
	"WHERE":                {},
	"WINDOW":               {},
	"WITH":                 {},
	"WITHIN":               {},
}

type spannerDriver struct{}

func (d *spannerDriver) Parse(_driverName, datasourceName string) (*core.Uri, error) {
	return &core.Uri{DbType: "spanner", DbName: datasourceName}, nil
}

type spanner struct {
	core.Base
}

func (s *spanner) Init(db *core.DB, uri *core.Uri, driverName string, datasourceName string) error {
	return s.Base.Init(db, s, uri, driverName, datasourceName)
}
func (s *spanner) Filters() []core.Filter { return []core.Filter{&core.IdFilter{}} }
func (s *spanner) IsReserved(name string) bool {
	_, exists := spannerReservedKeywords[name]
	return exists
}
func (s *spanner) AndStr() string      { return "AND" }
func (s *spanner) OrStr() string       { return "OR" }
func (s *spanner) EqStr() string       { return "=" }
func (s *spanner) RollBackStr() string { return "ROLL BACK" }
func (s *spanner) AutoIncrStr() string {
	// Spanner does not support auto-increment, but supports unique generated IDs (not sequential!).
	return "GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE)"
}
func (s *spanner) SupportInsertMany() bool   { return false } // Needs manual transaction batching
func (s *spanner) SupportEngine() bool       { return false } // No support for engine selection
func (s *spanner) SupportCharset() bool      { return false } // ...or charsets
func (s *spanner) SupportDropIfExists() bool { return false } // Drop should be handled differently
func (s *spanner) IndexOnTable() bool        { return false }
func (s *spanner) ShowCreateNull() bool      { return false }
func (s *spanner) Quote(name string) string  { return "`" + name + "`" }
func (s *spanner) SqlType(col *core.Column) string {
	switch col.SQLType.Name {
	case core.Int, core.SmallInt, core.BigInt:
		return "INT64"
	case core.Varchar, core.Text, core.MediumText, core.LongText, core.Char, core.NVarchar, core.NChar, core.NText:
		l := col.Length
		if l == 0 {
			l = col.SQLType.DefaultLength
		}
		if l > 0 {
			return fmt.Sprintf("STRING(%d)", l)
		}
		return "STRING(MAX)"
	case core.Jsonb:
		return "STRING(MAX)"
	case core.Bool, core.TinyInt:
		return "BOOL"
	case core.Float, core.Double:
		return "FLOAT64"
	case core.Bytea, core.Blob, core.MediumBlob, core.LongBlob:
		l := col.Length
		if l == 0 {
			l = col.SQLType.DefaultLength
		}
		if l > 0 {
			return fmt.Sprintf("BYTES(%d)", l)
		}
		return "BYTES(MAX)"
	case core.DateTime, core.TimeStamp:
		return "TIMESTAMP"
	default:
		panic("unknown column type: " + col.SQLType.Name)
		//default:
		//	return "STRING(MAX)" // XXX: more types to add
	}
}

func (s *spanner) GetColumns(tableName string) ([]string, map[string]*core.Column, error) {
	query := `SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE, IS_IDENTITY, IDENTITY_GENERATION, IDENTITY_KIND, COLUMN_DEFAULT
				FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND TABLE_SCHEMA="" ORDER BY ORDINAL_POSITION`
	rows, err := s.DB().Query(query, tableName)
	if err != nil {
		return nil, nil, err
	}
	defer rows.Close()

	columns := make(map[string]*core.Column)
	var colNames []string

	var name, sqlType, isNullable string
	var isIdentity, identityGeneration, identityKind, columnDefault sql.NullString
	for rows.Next() {
		if err := rows.Scan(&name, &sqlType, &isNullable, &isIdentity, &identityGeneration, &identityKind, &columnDefault); err != nil {
			return nil, nil, err
		}

		var length int
		switch {
		case sqlType == "INT64":
			sqlType = core.Int
		case sqlType == "FLOAT32" || sqlType == "FLOAT64":
			sqlType = core.Float
		case sqlType == "BOOL":
			sqlType = core.Bool
		case sqlType == "BYTES(MAX)":
			sqlType = core.Blob
		case sqlType == "STRING(MAX)":
			sqlType = core.NVarchar
		case sqlType == "TIMESTAMP":
			sqlType = core.DateTime
		case strings.HasPrefix(sqlType, "BYTES("):
			// 6 == len(`BYTES(`), we also remove ")" from the end.
			if l, err := strconv.Atoi(sqlType[6 : len(sqlType)-1]); err == nil {
				length = l
			}
			sqlType = core.Blob
		case strings.HasPrefix(sqlType, "STRING("):
			// 7 == len(`STRING(`), we also remove ")" from the end.
			if l, err := strconv.Atoi(sqlType[7 : len(sqlType)-1]); err == nil {
				length = l
			}
			sqlType = core.Varchar
		default:
			panic("unknown column type: " + sqlType)
		}

		autoincrement := isIdentity.Valid && isIdentity.String == "YES" &&
			identityGeneration.Valid && identityGeneration.String == "BY DEFAULT" &&
			identityKind.Valid && identityKind.String == "BIT_REVERSED_POSITIVE_SEQUENCE"

		defValue := ""
		defEmpty := true
		if columnDefault.Valid {
			defValue = columnDefault.String
			defEmpty = false
		}

		col := &core.Column{
			Name:            name,
			SQLType:         core.SQLType{Name: sqlType},
			Length:          length,
			Nullable:        isNullable == "YES",
			IsAutoIncrement: autoincrement,
			Indexes:         map[string]int{},
			Default:         defValue,
			DefaultIsEmpty:  defEmpty,
		}
		columns[name] = col
		colNames = append(colNames, name)
	}

	return colNames, columns, rows.Err()
}

func (s *spanner) CreateTableSql(table *core.Table, tableName, _, charset string) string {
	sql := "CREATE TABLE " + s.Quote(tableName) + " ("

	for i, col := range table.Columns() {
		if i > 0 {
			sql += ", "
		}

		sql += s.Quote(col.Name) + " " + s.SqlType(col)
		if !col.Nullable {
			sql += " NOT NULL"
		}
		if col.Default != "" {
			sql += " DEFAULT (" + col.Default + ")"
		}
		if col.IsAutoIncrement {
			sql += " GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE)"
		}
	}

	sql += ") PRIMARY KEY (" + strings.Join(table.PrimaryKeys, ",") + ")"
	return sql
}

func (s *spanner) CreateIndexSql(tableName string, index *core.Index) string {
	sql := "CREATE "
	if index.Type == core.UniqueType {
		sql += "UNIQUE NULL_FILTERED "
	}
	sql += "INDEX " + s.Quote(index.XName(tableName)) + " ON " + s.Quote(tableName) + " (" + strings.Join(index.Cols, ", ") + ")"
	return sql
}

func (s *spanner) IndexCheckSql(tableName, indexName string) (string, []any) {
	return `SELECT index_name FROM information_schema.indexes
	        WHERE table_name = ? AND table_schema = "" AND index_name = ?`,
		[]any{tableName, indexName}
}

func (s *spanner) TableCheckSql(tableName string) (string, []any) {
	return `SELECT table_name FROM information_schema.tables
	        WHERE table_name = ? AND table_schema = ""`,
		[]any{tableName}
}

func (s *spanner) GetTables() ([]*core.Table, error) {
	res, err := s.DB().Query(`SELECT table_name FROM information_schema.tables WHERE table_schema = ""`)
	if err != nil {
		return nil, err
	}
	defer res.Close()

	tables := []*core.Table{}
	for res.Next() {
		var name string
		if err := res.Scan(&name); err != nil {
			return nil, err
		}
		t := core.NewEmptyTable()
		t.Name = name
		tables = append(tables, t)
	}
	return tables, res.Err()
}

func (s *spanner) GetIndexes(tableName string) (map[string]*core.Index, error) {
	res, err := s.DB().Query(`SELECT ix.INDEX_NAME, ix.INDEX_TYPE, ix.IS_UNIQUE, c.COLUMN_NAME
		FROM INFORMATION_SCHEMA.INDEXES ix
		JOIN INFORMATION_SCHEMA.INDEX_COLUMNS c ON (ix.TABLE_NAME=c.TABLE_NAME AND ix.INDEX_NAME=c.INDEX_NAME)
		WHERE ix.TABLE_SCHEMA = "" AND ix.TABLE_NAME=?
		ORDER BY ix.INDEX_NAME, c.ORDINAL_POSITION`, tableName)
	if err != nil {
		return nil, err
	}
	defer res.Close()

	indexes := map[string]*core.Index{}
	var ixName, ixType, colName string
	var isUnique bool
	for res.Next() {
		err := res.Scan(&ixName, &ixType, &isUnique, &colName)
		if err != nil {
			return nil, err
		}

		isRegular := false
		if strings.HasPrefix(ixName, "IDX_"+tableName) || strings.HasPrefix(ixName, "UQE_"+tableName) {
			ixName = ixName[5+len(tableName):]
			isRegular = true
		}

		var index *core.Index
		var ok bool
		if index, ok = indexes[ixName]; !ok {
			t := core.IndexType // ixType == "INDEX" && !isUnique
			if ixType == "PRIMARY KEY" || isUnique {
				t = core.UniqueType
			}

			index = &core.Index{}
			index.IsRegular = isRegular
			index.Type = t
			index.Name = ixName
			indexes[ixName] = index
		}
		index.AddColumn(colName)
	}
	return indexes, res.Err()
}

func (s *spanner) CreateSequenceGenerator(db *sql.DB) (SequenceGenerator, error) {
	dsn := s.DataSourceName()
	connectorConfig, err := spannerdriver.ExtractConnectorConfig(dsn)
	if err != nil {
		return nil, err
	}

	if connectorConfig.Params[strings.ToLower("inMemSequenceGenerator")] == "true" {
		// Switch to using in-memory sequence number generator.
		// Using database-based sequence generator doesn't work with emulator, as emulator
		// only supports single transaction. If there is already another transaction started
		// generating new ID via database-based sequence generator would always fail.
		return newInMemSequenceGenerator(), nil
	}

	return newSequenceGenerator(db), nil
}

func (s *spanner) RetryOnError(err error) bool {
	return err != nil && spannerclient.ErrCode(spannerclient.ToSpannerError(err)) == codes.Aborted
}
